package astra.server.security;

import astra.client.models.Employee;
import astra.client.models.WrongCredentialsException;
import astra.server.db.DatabaseWorker;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPCRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

/**
 * @author Kotelnikov Dmitry <a href="mailto:dimonnot@mail.ru"/>
 */
public class AccessControlServlet extends DatabaseWorker {
    public static final Log log = LogFactory.getLog(AccessControlServlet.class);

    public static final int SESSION_TIMEOUT = 1000 * 60 * 60 * 24;

    public static final String PERMISSIONS[] = new String[]{
        "supplier","purchase","client","sale","cost","invoice","employee"
    };

	private static final String SALT = "astra";
	private final Object monitor = new Object();
	private final Map<String, Employee> sessions = new HashMap<String, Employee>();

	protected ThreadLocal<Employee> threadLocalEmployee = new ThreadLocal<Employee>();

	@Override
	public void init() throws ServletException {
		super.init();
	}

	protected boolean checkAccess() {
		String sessionId = null;
		Cookie cookies[] = getThreadLocalRequest().getCookies();
		if (cookies != null) {
			for (int i = 0; i < cookies.length; i++) {
				if ("sessionId".equals(cookies[i].getName())) {
					sessionId = cookies[i].getValue();
				}
			}
		}
		if (sessionId != null) {
            Employee employee = null;
			synchronized(monitor) {
				employee = sessions.get(sessionId);
			}

			if (employee != null) {
                /*
				 * Check User-Agent and user IP for session
				 */
				if (
					sessionId.equals(
						generateSessionId(employee.getId(), getThreadLocalRequest())
					)
				) {
                    if (
						System.currentTimeMillis() - employee.getLastVisitDate().getTime()
						< SESSION_TIMEOUT
					) {
                        employee.setLastVisitDate(new Date());
						threadLocalEmployee.set(employee);
						return true;
					}
					//Session expired
					synchronized (monitor) {
						sessions.remove(sessionId);
					}
				}
			}
		}
		return false;
	}

	@Override
	public String processCall(String payload) throws SerializationException {
		try {
			return super.processCall(payload);
		} catch (AccessDeniedException e) {
			return "Access denied";
		}
	}

	@Override
	protected void onAfterRequestDeserialized(RPCRequest rpcRequest) {
        if (!"signIn".equals(rpcRequest.getMethod().getName()) && !checkAccess()) {
			throw new AccessDeniedException("Access denied");
		}
	}

	public String md5(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("md5");
            digest.update(value.getBytes());
            byte[] hash = digest.digest();
            StringBuilder buffer = new StringBuilder(hash.length * 2);
            int i;
            for (i = 0; i < hash.length; i++) {
                if ((hash[i] & 0xff) < 0x10)
                    buffer.append("0");
                buffer.append(Long.toString(hash[i] & 0xff, 16));
            }
            return buffer.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	public Employee signIn(String username, String password) throws WrongCredentialsException {
		List<Employee> employees = jdbcTemplate.query(
			"select e.*, g.*, g.name as `group_name` from `employee` e, `group` g " +
            "where `login` = ? and `password` = ? and g.id = e.group_id",
			new ParameterizedRowMapper<Employee>(){
				@Override
				public Employee mapRow(ResultSet rs, int rowNum)
						throws SQLException {
					Map<String, Employee.Access> permissions = new HashMap<String, Employee.Access>();
                    for (String permission: PERMISSIONS) {
                        permissions.put(permission, Employee.Access.values()[rs.getInt(permission)]);
                    }
					return new Employee(
						rs.getInt("id"),
						rs.getString("login"),
						rs.getString("name"),
                        rs.getInt("group_id"),
						new Date(),
                        rs.getString("group_name"),
                        rs.getString("phone"),
                        rs.getString("address"),
						permissions
					);
				}
			},
			username,
			md5(SALT + password)
		);

        if (!employees.isEmpty()) {
			Employee employee = employees.get(0);
			String sessionId = generateSessionId(employee.getId(), getThreadLocalRequest());
            getThreadLocalResponse().addCookie(new Cookie("sessionId", sessionId));
			synchronized (monitor) {
                sessions.put(sessionId, employee);
			}
			removeExpiredSessions();
            return employee;
		}
		throw new WrongCredentialsException();
	}

	protected String generateSessionId(int employeeId, HttpServletRequest request) {
		String session = request.getRemoteAddr();
		String userAgent = request.getHeader("User-Agent");
		if (userAgent != null) {
			session += userAgent;
		}
		return employeeId + md5(session);
	}

	protected void removeExpiredSessions() {
		synchronized (monitor) {
			for(
				Iterator<Map.Entry<String, Employee>> iterator =
				sessions.entrySet().iterator(); iterator.hasNext();
			) {
				Map.Entry<String, Employee> entry = iterator.next();
				if (
					System.currentTimeMillis() - entry.getValue().getLastVisitDate().getTime()
					>= SESSION_TIMEOUT
				) {
					iterator.remove();
				}
			}
		}
	}

	protected void removeEmployeeSessions(int employeeId) {
		synchronized (monitor) {
			for(
				Iterator<Map.Entry<String, Employee>> iterator =
				sessions.entrySet().iterator(); iterator.hasNext();
			) {
				Map.Entry<String, Employee> entry = iterator.next();
				if (entry.getValue().getId() == employeeId) {
					iterator.remove();
				}
			}
		}
	}
}
